package com.seednia.foundation.framework.service;

import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.aop.framework.Advised;
import org.springframework.aop.support.AopUtils;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

import com.seednia.foundation.framework.dto.AbstractRecord;


/**
 * Singleton class that serves as the access point for all the available services.
 * The application will be able to obtain the services from this object.
 * The services are loaded using Spring IoC, by default from service-bean.xml
 * file in the root class path.
 * 
 * @author Young Suk Ahn
 *
 */
public class ServiceContainer {
	
	private final String DEFAULT_FILEPATH = "service-beans.xml";

	protected static final ServiceContainer instance = new ServiceContainer();
	
	private Map<String, IService> serviceRegistryMap = Collections.synchronizedMap(new HashMap<String, IService>() );
	
	private static final Log logger = LogFactory.getLog(ServiceContainer.class);
	
	/**
	 * Prevents from explicitly instantiating this class
	 */
	private ServiceContainer(){}
	
	/**
	 * Get's the singleton instance
	 * @return
	 */
	public static ServiceContainer getInstance()
	{
		return instance;
	}
	
	/**
	 * Loads from a XML bean description file using Spring DI
	 * @param filename
	 */
	public void loadAppContext(String filename)
	{
		if (filename == null)
			filename = DEFAULT_FILEPATH; 
		// Spring bean ApplicationContext
		
		logger.debug("Loading XmlApplicationContext '"+filename+"'");
		ApplicationContext context = new ClassPathXmlApplicationContext(filename);
		
		int numServices = loadServices(context);		
		logger.info("Loaded "+numServices+" services");
	}
	
	/**
	 * Load to the service container beans that 
	 * @param appContext
	 */
	public int loadServices(ApplicationContext appContext)
	{
		Map<String, IService> services = appContext.getBeansOfType(IService.class);
		
		int svcCtr = 0;
		for (Map.Entry<String, IService> entry : services.entrySet()) {
			entry.getValue().initialize();
			// If id property is not set, use the bean's id
			if (entry.getValue().getId() == null)
				entry.getValue().setId(entry.getKey());

			svcCtr++;
			this.register(entry.getValue());
		}
		
		return svcCtr;
	}
	
	/**
	 * Registers a service
	 * @param service
	 */
	public void register(IService service)
	{
		if (service.getId() != null)
			register(service.getId(), service);
		else 
			register(service.getClass().getName(), service);
	}

	/**
	 * Register a service to the container using a user defined id
	 * @param id
	 * @param service
	 */
	public void register(String id, IService service)
	{
		logger.debug("Registering service ["+id+"]");
		serviceRegistryMap.put(id, service);
	}
	
	public Collection<IService> services()
	{
		return this.serviceRegistryMap.values();
	}

	/**
	 * Retrieves the services given it's type
	 * @param serviceType
	 * @return
	 */
	public IService get(Object param)
	{
		if (param instanceof String)
			return getById( (String)param );
		else
			return getByType( (Class)param );
	}
	
	/**
	 * Retrieves the service by its id
	 * @param serviceType
	 * @return
	 */
	public IService getById(String id)
	{
		return serviceRegistryMap.get(id);
	}
	
	
	/**
	 * Retrieves the first service that exactly matches the type
	 * @param serviceType
	 * @return
	 */
	public <T extends IService> T getByType(Class<T> serviceType)
	{
		for (Map.Entry<String, IService> entry : serviceRegistryMap.entrySet()) {
			IService targetObj = entry.getValue();
			// Check whether it's a proxy 
			if (AopUtils.isAopProxy(entry.getValue())) {
				try {
					targetObj = (IService)((Advised)entry.getValue()).getTargetSource().getTarget();
				} catch (Exception e) {
					throw new RuntimeException(e);
				}
			}
			if (serviceType.isInstance(targetObj)) {
		    	return serviceType.cast(entry.getValue());
		    }
		}
		return null;
	}
	

	/**
	 * Retrieves list of services that matches the provided type
	 * @param serviceType
	 * @return
	 * @throws Exception 
	 */
	public <T> List<T> getListByType(Class<T> serviceType) 
	{
		List<T> retval = new ArrayList<T>();
		for (Map.Entry<String, IService> entry : serviceRegistryMap.entrySet()) {
			IService targetObj = entry.getValue();
			// Check whether it's a proxy 
			if (AopUtils.isAopProxy(entry.getValue())) {
				try {
					targetObj = (IService)((Advised)entry.getValue()).getTargetSource().getTarget();
				} catch (Exception e) {
					throw new RuntimeException(e);
				}
			}
			if (serviceType.isInstance(targetObj)) {
				retval.add( (T)entry.getValue());
		    }
		}
		
		// Sort based on IService.order
		Collections.sort(retval, new ServiceComparator());

		
		return retval;
	}

	
	/**
	 * Returns a DataOrientedService that matches the record class type
	 * @param recordType type of the DTO used by the service object
	 * @return
	 */
	public IDataOrientedService getDataOrientedService(Class<?> recordType)
	{
		IDataOrientedService retval = null;
		
		for (Map.Entry<String, IService> entry : serviceRegistryMap.entrySet()) {
		    if (entry.getValue() instanceof IDataOrientedService)
		    {
		    	IDataOrientedService dos = (IDataOrientedService)entry.getValue();
		    	// [11/06/09] Changed so it can identify even subclasses
		    	if (dos.getRecordClass().isAssignableFrom(recordType))
		    	{
		    		retval =  dos;
		    		break;
		    	}
		    }
		}
		
		return retval;
	}
	
	/**
	 * Returns a DataOrientedService that matches the record class type
	 * @param recordType type of the DTO used by the service object
	 * @return
	 */
	public List<IDataOrientedService> getDataOrientedServiceWithAnnotatedRecord(Class<? extends Annotation> annotation)
	{
		List<IDataOrientedService> retval = new ArrayList<IDataOrientedService>() ;
		
		for (Map.Entry<String, IService> entry : serviceRegistryMap.entrySet()) {
		    if (entry.getValue() instanceof IDataOrientedService)
		    {
		    	IDataOrientedService dos = (IDataOrientedService)entry.getValue();
		    	Class<?> recordClass = dos.getRecordClass();
		    	Object annotationObj = recordClass.getAnnotation(annotation) ;
		    	//annotationObj;
		    	if (dos.getRecordClass().getAnnotation(annotation) != null)
		    	{
		    		retval.add(dos);
		    	}
		    }
		}
		
		return retval;
	}
	
}
